from __future__ import print_function
from __future__ import absolute_import

import stackless
import sys
import logging
import weakref
import traceback
import time
import abc
import os
from os import path

import configobj

from kharlia.__core import *
from kharlia._core import *

# Monkeypatch _socket
#sys.modules['_socket'] = _core._slsocket

# CONSTANTS
# Service states
SVC_STOPPED = 0
SVC_STARTED = 1
SVC_PAUSED = 2
SVC_ERROR = 3
SVC_STARTING = 4
SVC_STOPPING = 5

# App states
APP_STOPPED = 0
APP_STARTING = 1
APP_STARTED = 2
APP_STOPPING = 3

# GLOBALS

GLogger = None

def GetMainLogger():
    return GLogger

# NATIVE CLASS SUPPORT

def NativeClass(cls):
    '''
    Class decorator to specifying the native base class created with 
    Boost Python. The native base should be the first base class in the class
    definition.
    '''
    nativebase = cls.__bases__[0]
    assert cls.__base__ is nativebase, \
           'The native base must be the solid base (__base__)'
    assert isinstance(nativebase, GetBoostPythonClassType()), \
           'The native base must be an instance of Boost.Python.class'
    #assert cls.__name__ == nativebase.__name__, \
    #       'The native base must have the same name as the native class'
    cls.NativeBase = nativebase
    return cls

@NativeClass
class CObject(CObjectBase):
    '''
    This is the base class for object that implement advanced native
    functionality. The native code of this class allows native
    objects to retrieve a reference to self, allowing them to easily
    access python code from native code.
    
    This functionality is used to implement partial native functions,
    where native code is wrapped by python code.

    Multiple inheritence is not supported of more than one
    CObject derived class in Python or native code, exlcuding
    a Python's classes single native implementation class.
    '''
    def __init__(self):
        pass

    def __initnative__(self, cls=None, *args, **kwds):
        return CObject.InitNative(cls, self, *args, **kwds)

# CONFIG

class ConfigError(KharliaException):
    pass

class ConfigMergeError(ConfigError):
    pass

def _ConvertJSONUnicodeToStr(jdict):
    ndict = None
    if isinstance(jdict, dict):
        ndict = {}
        for key, value in jdict.iteritems():
            if isinstance(value, dict):
                ndict[str(key)] = converter(value)
            elif isinstance(value, list):
                ndict[str(key)] = converter(value)
            elif isinstance(value, unicode):
                ndict[str(key)] = str(value)
            else:
                ndict[str(key)] = value
    elif isinstance(jdict, list):
       ndict = []
       for value in jdict:
           if isinstance(value, unicode):
               ndict.append(str(value))
           else:
               ndict.append(value)
    return ndict

def LoadConfig(filename, merge=True, basefilename=None, _topcfg=True):
    '''
    Loads a ConfigObj file from disk using unrepr mode. If merge is true
    and a base file is specified in the config file, the file is
    merged with the base config file, but the values in the base config
    will not overwrite.
    '''
    if merge:
        GLogger.info('loading config: %s', filename)
        cfg = configobj.ConfigObj(filename, unrepr=True)

        # If we've created a new file, add the base
        if len(cfg) == 0 and basefilename is not None:
            cfg['Base'] = basefilename

        # If a base file is specified, merge it
        if 'Base' in cfg:
            basefilename = cfg['Base']
            if not isinstance(basefilename, basestring):
                raise TypeError('base config filename must be a string')
                
            # Load the config file, it will merge its own base
            cfgbase = LoadConfig(basefilename, _topcfg=False)
            
            # We have to use our own merge function because we want
            # behavior opposite to the default merge operation.
            def merge(indict, indictbase):
                for key, val in indictbase.iteritems():
                    haskey = key in indict
                    if (haskey and isinstance(indict[key], dict) and
                                   isinstance(val, dict)):
                        merge(indict[key], val)
                    elif not haskey:
                        indict[key] = val
            
            # Merge the configs
            merge(cfg, cfgbase)
    
    else:
        GLogger.info('loading config (no merge): %s', filename)
        cfg = configobj.ConfigObj(filename, unrepr=True)

    # Correct indent type for newly created configs
    if cfg.indent_type == '':
        cfg.indent_type = '    '

    return cfg

# EVENT

class FEvent(object):
    __slots__ = ['_Binds', '_CreateTasklet']
    def __init__(self, create_tasklet=None):
        self._Binds = weakref.WeakKeyDictionary()
        if create_tasklet is None:
            create_tasklet = CTasker.GetDefault().CreateTasklet
        self._CreateTasklet = create_tasklet
    
    def __call__(self, *args, **kwds):
        if len(self._Binds) < 1:
            return
        ct = self._CreateTasklet
        for obj, attrname in self._Binds.iteritems():
            if attrname is None:
                ct(obj)(*args, **kwds)
            else:
                ct(getattr(obj, attrname))(*args, **kwds)
    
    def Bind(self, obj, attrname=None):
        self._Binds[obj] = attrname
    
    def Unbind(self, obj):
        del self._Binds[obj]
        
class FDirectEvent(FEvent):
    __slots__ = []
    def __call__(self, *args, **kwds):
        if len(self._Binds) < 1:
            return
        for obj, attrname in self._Binds.iteritems():
            if attrname is None:
                obj(*args, **kwds)
            else:
                getattr(obj, attrname)(*args, **kwds)

# SUBSYSTEM

@NativeClass
class CSubsystem(CSubsystemBase, CObject):
    def __init__(self, *args, **kwds):
        CObject.__init__(self, *args, **kwds)
        self.__initnative__()

    def Tick(self, deltaTime):
        raise NotImplementedError()

# TASKER

@NativeClass
class CTasker(CTaskerBase, CSubsystem):
    def __init__(self, *args, **kwds):
        self.__initnative__(None, *args, **kwds)
        CSubsystem.__init__(self)

# TODO: Make actual static versions of Create and InsertTasklet

_CreateTaskletImpl = None
_InsertTaskletImpl = None
    
def CreateTasklet(*args, **kwds):
    return _CreateTaskletImpl(*args, **kwds)
    
def InsertTasklet(*args, **kwds):
    return _InsertTaskletImpl(*args, **kwds)

# IO

@NativeClass
class CIOManager(CIOManagerBase, CObject):
    def __init__(self, poolsize=3):
        self.__initnative__(None, poolsize)
        CObject.__init__(self)

# SERVICE

class ServiceError(KharliaException):
    '''
    Base class for all service-related errors.
    '''

class ServiceTransitionError(ServiceError):
    '''
    Raised when an error occurs while a service is transitioning from one
    state to another.
    '''
    
class ServiceStateError(ServiceError):
    '''
    Attempted to perform an action on a service that was not appropriate for
    its current state.
    '''

class CService(object):
    '''
    CService(Manager, ConfigDict=None)
    
    The base class of all services. This is an abstract class defines the
    properties and functions that all services must share.
    '''
    __metaclass__ = abc.ABCMeta
    Name = None
    Depends = None
    Manual = False
    
    def __init__(self, manager, config=None):
        self.Manager = manager
        self.Config = config
        self._IsStarted = False
        self.EventStarted = FEvent()
        self.EventStopped = FEvent()
    
    @property
    def IsStarted(self):
        return self._IsStarted
    
    def Start(self):
        if self.IsStarted:
            return
        GLogger.info('starting service: ' + self.Name)
        # Start all dependencies for this service
        for i in self.Depends:
            try:
                svc = self.Manager.GetServiceNoFail(i)
                if not svc.IsStarted:
                    svc.Start()
            except ServiceError as e:
                raise ServiceTransitionError('Unable to Start service {0}, '
                                             'dependency not found: {1}'
                                             .format(self.Name, e.message))
        # Start implementation for subclasses
        self.OnStart()
        self._IsStarted = True
        self.EventStarted()
        #GLogger.debug('started service: ' + self.Name)

    def Stop(self):
        if not self.IsStarted:
            return
        GLogger.info('stopping service: ' + self.Name)
        # Stop any services that depend on this one
        for i in self.Manager.Services.itervalues():
            if self.Name in i.Depends:
                if i.IsStarted:
                    i.Stop()
        self.OnStop()
        self._IsStarted = False
        #GLogger.debug('stopped service: ' + self.Name)
        
    def Restart(self):
        '''
        Calls a Start and Stop of the service.
        '''
        GLogger.info('restarting service: ' + self.Name)
        self.Stop()
        self.Start()
    
    @abc.abstractmethod
    def OnStart(self):
        pass

    @abc.abstractmethod
    def OnStop(self):
        pass
            
    def __repr__(self):
        return '<kharlia.core.CService \'{0}\'>'.format(self.Name)

class CTaskletService(CService):
    '''
    The base class for Tasklet-based services. Services that use tasklets in
    operation should subclass this class.
    '''
    __metaclass__ = abc.ABCMeta
    
    def __init__(self, manager, config=None):
        CService.__init__(self, manager, config)
        self.TargetPath = None
        self.Tasklet = None
        self.TaskletRunning = False
        self.KillingTasklet = False
        self.StopRequested = False
        self.RunOnStart = config.get('RunOnStart', False)
        
    def TaskletStartup(self):
        try:
            self.TaskletRunning = True
            self.TaskletMain()            
        finally:
            self.TaskletRunning = False
            if self.KillingTasklet:
                GLogger.debug('Tasklet for service {0} exiting on kill'
                              .format(self.Name))
            else:
                GLogger.debug('Tasklet for service {0} exiting on error'
                              .format(self.Name))
    
    def TaskletMain(self):
        if not self.TargetPath:
            raise ServiceError('no target path for service: ' + self.Name)
        target = ResolveName(self.TargetPath)
        target(self, *self.Args)
    
    def Start(self):
        super(CTaskletService, self).Start()
        if self.RunOnStart:
            self.Tasklet.run()

    def OnStart(self):
        self.Tasklet = CTasker.GetDefault().CreateTasklet(self.TaskletStartup)()
    
    def OnStop(self):
        self.KillingTasklet = True
        self.Tasklet.kill()
        self.KillingTasklet = False
        self.Tasklet = None

class CServiceManager(object):
    def __init__(self, config):
        self.Services = {}
        self.Config = config
        if config is not None:
            self.LoadServices()

    def GetService(self, classname):
        return self.Services.get(classname, None)
    
    def GetServiceNoFail(self, classname):
        try:
            return self.Services[classname]
        except KeyError:
            raise ServiceError('service not found with name: {0}'.format(classname))
    
    def LoadServices(self):
        if self.Config is None:
            raise RuntimeError('no config specified')

        for k, v in self.Config.iteritems():
            # Skip this entry if disabled
            if not v.get('Enabled', False):
                continue
            GLogger.debug('loading service: ' + k)
            if k in self.Services:
                raise ServiceError('service already exists: ' + k)
                
            # Try resolving the name to get the class
            cls = None
            try:
                cls = ResolveName(k)
            except ResolutionError:
                GLogger.warning('unable to resolve service name: %s', k)
                continue
            
            # Check for problems
            if cls is CService or cls is CTaskletService:
                raise TypeError('service class cannot be CService or CTaskletService')
            if not issubclass(cls, CService):
                raise TypeError('service class must be a subclass of CService')
            if cls.Name is None:
                raise ValueError('service name undefined')

            svc = cls(self, v)
            self.Services[k] = svc

    def StartServices(self, all=False):
        GLogger.debug('starting services')
        start = time.clock()
        for svc in self.Services.itervalues():
            if not all and svc.Manual:
                continue
            if not svc.IsStarted:
                svc.Start()
        elapsed = time.clock() - start
        GLogger.debug('started services in %s seconds', elapsed)
    
    def StopServices(self, all=False):
        GLogger.debug('stopping services')
        start = time.clock()
        for svc in self.Services.itervalues():
            if not all and svc.Manual:
                continue
            if svc.IsStarted:
                svc.Stop()
        elapsed = time.clock() - start
        GLogger.debug('stopped services in %s seconds', elapsed)

# APP

class AppError(KharliaException):
    '''
    Base class for exceptions thrown by kharlia.core.CApp
    '''

@NativeClass
class CApp(CAppBase, CObject):
    '''
    Central application class, provides Tasklet, service, and subsystem managing,
    and the main loop.
    '''

    def __init__(self, appid, configdir='config', name=None):
        # Pre-init checks
        if not isinstance(appid, str):
            raise TypeError('appid must be instance of str')
        ct = stackless.getcurrent()
        if not ct.is_main:
            raise RuntimeError('App must be created on the main Tasklet')
        ct.block_trap = True
        # Initialization
        self.__initnative__()
        CObject.__init__(self)
        
        self.AppID = intern(appid)
        self.Args = None
        self.Config = None
        self.ConfigRoot = None
        self.ConfigDir = configdir
        self.Name = None
        self.State = APP_STOPPED
        self.ExitMonitorTasklet = None
        self.EventExitRequested = None
        
        self.ServiceManager = None

    def IterSubsystems(self):
        subsystems = self.GetSubsystemList()
        for index, subsystem in subsystems:
            yield index, subsystem

    def RequestExit(self):
        if self.ExitRequested:
            return
        super(CApp, self).RequestExit()

        GLogger.debug('exiting run in %s seconds', self.ExitWaitTime)

        self.StartExitMonitor()     
        t = CreateTasklet(self.ServiceManager.StopServices)(all=True)
        t.critical = True
        self.EventExitRequested()

    def StartExitMonitor(self):
        taskers = []
        for index, subsystem in self.IterSubsystems():
            if isinstance(subsystem, CTasker):
                taskers.append(subsystem)
        
        if len(taskers) > 0:
            t = CreateTasklet(self.ExitMonitorMain)
            t.critical = True
            t(taskers, self.ForceExit)

    @property
    def CurrentTasklet(self):
        '''
        Alias for stackless.getcurrent(). May be extended in future.
        '''
        return stackless.getcurrent()
    
    def ExitMonitorMain(self, taskers, callback, interval=0.2):
        '''
        Exit monitor tasklet that allows tasklet managers to wind down before
        executing a callback. The purposes of this is to allow tasklets to execute
        in response to the program shutting down, without simply waiting X number
        of seconds before killing all tasklets. Current tasklet is excluded
        from the check.
        
        Arguments:
        taskers -- the list of taskers to check
        callback -- the object to call when all tasklets are dead
        interval -- the time to wait between checks (default 0.2)
        '''
        GLogger.debug('starting exit monitor')
        try:
            current = stackless.getcurrent()
            assert isinstance(current, FTaskletExt)
            sleep = Sleep
            done = False
            while self.IsRunning and not done:
                count = 0
                for tasker in taskers:
                    remaining = tasker.GetTaskletList(CTasker.ALL_GROUPS)
                    
                    # Don't count the current tasklet
                    if current in remaining:
                        count += len(remaining) - 1
                    else:
                        count += len(remaining)
                    
                if count > 0:
                    #GLogger.debug('tasklets remaining, sleeping for %s', interval)
                    sleep(interval)
                else:
                    GLogger.debug('tasklets are dead, exiting')
                    done = True
                    callback()
        finally:
            GLogger.debug('stopping exit monitor')

    def Run(self, args=[], trace=None, using_pydev=False):
        '''
        Run the application.
        
        Steps:
            1. Set logger class to FLogger.
            2. Initialize GLogger to logger 'core'
            3. Load configuration file.
            4. Create the default tasker.
            5. Create the Stackless IO manager.
            6. Create the service manager and start all non-manual services
               if Core.App.AutoStartServices is True.
            7. Iterate through Core.AutoExec and create a tasklet for each
               function.
            8. Call CAppBase.Run() to enter the main loop.
        '''
        global GLogger
        global _CreateTaskletImpl, _InsertTaskletImpl
        
        self.Args = args
        
        # Set up the logger
        assert GLogger is None
        logging.setLoggerClass(FLogger)
        GLogger = logging.getLogger('core')
        assert type(GLogger) is FLogger

        # Begin initialization
        GLogger.info('running app: %s', self.AppID)
        self.State = APP_STARTING

        # Load configuration file
        cfg_filename = path.join(self.ConfigDir, '{0}.ini'.format(self.AppID))
        basecfg_filename = path.join(self.ConfigDir, '{0}_default.ini'.format(self.AppID))
        croot = self.ConfigRoot = LoadConfig(cfg_filename, merge=True,
                                             basefilename=basecfg_filename)
        config = self.Config = croot['Core']['App']
        #croot.write()
        
        # set app Name
        if self.Name is None:
            self.Name = config['Name']
        GLogger.info('app name: ' + self.Name)

        # Tracing
        SetTraceFunc(trace)
        if using_pydev:
            import pydevd_tracing
            FTaskletExt.SetTraceImpl = staticmethod(pydevd_tracing.SetTrace)

        # Create the tasklet manager
        FTaskletExt.ExcPrintFunc = GLogger.error
        FTaskletExt.ExcPrintFuncCritical = GLogger.critical
        if trace:
            FTaskletExt.UseTrace = True
        assert CTasker.GetDefault() is None
        self._Tasker = tmgr = CTasker()
        CTasker.SetDefault(tmgr)
        _CreateTaskletImpl = tmgr.CreateTasklet
        _InsertTaskletImpl = tmgr.InsertTasklet
        self.AddSubsystem(5, tmgr)
        
        self.EventExitRequested = FEvent(CreateTasklet)
        
        # Create the IO manager
        self._IOManager = CIOManager(poolsize=3)

        # Create the service Manager
        self.ServiceManager = CServiceManager(self.ConfigRoot['Core']['Services'])
        if config.get('AutoStartServices', True):
            self.ServiceManager.StartServices()
            
        # Create tasklets for autoexec functions
        for name, value in croot['Core']['AutoExec'].iteritems():
            if value is False:
                continue
            target = ResolveName(name)
            assert callable(target)
            CreateTasklet(target)()

        # Native run
        self.State = APP_STARTED
        GLogger.debug('enterting native run')
        super(CApp, self).Run()
        GLogger.debug('exited native run')
        self.State = APP_STOPPING
        
        self.ServiceManager = None
        
        # Kill all tasklets
        self.Tasker.KillTasklets(CTasker.ALL_GROUPS)
        self.RemoveSubsystem(self.Tasker)
        CTasker.SetDefault(None)
        _CreateTaskletImpl = None
        _InsertTaskletImpl = None
        self._Tasker = None
        self._IOManager = None
        
        FTaskletExt.ExcPrintFunc = print
        FTaskletExt.ExcPrintFuncCritical = print
        FTaskletExt.UseTrace = False
        FTaskletExt.SetTraceImpl = staticmethod(sys.settrace)

        SetTraceFunc(None)

        # Save any changes to config
        self.ConfigRoot.write()

        # Final cleanup
        self.State = APP_STOPPED
        GLogger.info('app has exited')
        GLogger = None
        